/*
 *  libKara - Library for basic instant messaging functions
 *  Copyright (C) 2007 Markus Karnik
 *
 *  This program is free software; you can redistribute it and/or modify it 
 *  under the terms of the GNU General Public License as published by the
 *  Free Software Foundation; either version 3 of the License, 
 *  or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
 *  for more details.
 *
 *  You should have received a copy of the GNU General Public License along 
 *  with this program; if not, see <http://www.gnu.org/licenses/>.
 *  
 */

package de.karnik.libkara.helper;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

import de.karnik.helper.LogFunctions;
import de.karnik.helper.StringFunctions;
import de.karnik.libkara.proto.xmpp.XmppSettings;

/**
 * Useful methods to handle, convert or create sasl auth for the xmpp protocol.
 * 
 * @author Markus Karnik - markus.karnik@my-design.org
 * @version 1.0
 * @since 1.0
 */
public class SaslHelper {

	/**
	 * The XMPP settings.
	 */
	private XmppSettings xmppSettings = null;
	
	/**
	 * The sasl realm value.
	 */
	private String realmValue = null;
	/**
	 * The sasl nonce value.
	 */
	private String nonceValue = null;
	/**
	 * The sasl cnonce value.
	 */
	private String cnonceValue = null;
	/**
	 * The sasl nc value.
	 */
	private int nc = 0;
	/**
	 * The qop nc value.
	 */
	private String qop = null;
	/**
	 * The sasl stale value.
	 */
	private boolean stale = false;
	/**
	 * The sasl utf8 value.
	 */
	private boolean utf8 = false;
	/**
	 * The sasl algorithm value.
	 */
	private boolean algorithm = false;
	/**
	 * The sasl maxbufValue value.
	 */
	private int maxbufValue = 0;
	/**
	 * The sasl cipherValue value.
	 */
	private String  cipherValue = null;
	
	/**
	 * Standard constructor. Hidden!
	 */
	private SaslHelper() {}
	
	/**
	 * Constructs a sasl helper object from the given XMPP settings.
	 * 
	 * @param xmppSettings The XMPP settings to use.
	 */
	public SaslHelper( XmppSettings xmppSettings ){
		this();
		this.xmppSettings = xmppSettings;
	}
	
	/**
	 * Creates a response for the given challenge.
	 * 
	 * @param challenge The challenge to respond to.
	 * @return The response.
	 */
	public String createResponse( String challenge ) {
		
		StringBuffer sb = new StringBuffer();
		
		if( this.realmValue == null && this.nonceValue == null  && this.cnonceValue == null ) { // initial auth
		
			this.realmValue = this.xmppSettings.getStringData( XmppSettings.STRING_VALUE_HOSTNAME );
			
			BigInteger bi = new BigInteger( 16, new Random() );
			this.cnonceValue = bi.toString();
			
			this.nc = 1;
			String newNc = StringFunctions.fillWithCharacter( String.valueOf( this.nc ), '0', 8 );
			
			this.interpretChallenge( challenge );
			
			sb.append( "username=\"" ).append( this.xmppSettings.getStringData( XmppSettings.STRING_VALUE_USERID ) ).append( "\"," );
			sb.append( "realm=\"" ).append( this.realmValue ).append( "\"," );
			sb.append( "nonce=\"" ).append( this.nonceValue ).append( "\"," );
			sb.append( "cnonce=\"" ).append( this.cnonceValue ).append( "\"," );
			sb.append( "nc=" ).append( newNc ).append( ',' );
			sb.append( "qop=" ).append( this.qop ).append( ',' );
			sb.append( "digest-uri=\"xmpp/" ).append( this.realmValue ).append( "\"," );
			sb.append( "response=" ).append( computerResponseValue() ).append( ',' );
			sb.append( "charset=utf-8" );
			
		}
		
		return sb.toString();
	}
	
	/**
	 * Computes the response value.
	 * 
	 * @return The response value.
	 */
	private String computerResponseValue() {

		String response = null;
		
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            final byte[] COLON = { 0x3a };
            
            // Calculate H(A1)
            md5.reset();
            md5.update( this.xmppSettings.getStringData( XmppSettings.STRING_VALUE_USERID ).getBytes( "utf-8" ) );
            md5.update( COLON );
            md5.update( this.realmValue.getBytes( "utf-8" ) );
            md5.update( COLON );
            md5.update( this.xmppSettings.getStringData( XmppSettings.STRING_VALUE_PASSWORD ).getBytes( "utf-8" ) );
            byte[] ha1 = md5.digest();

            md5.reset();
            md5.update( ha1 );
            md5.update( COLON );
            md5.update( this.nonceValue.getBytes( "utf-8" ) );
            md5.update( COLON );
            md5.update( this.cnonceValue.getBytes( "utf-8" ) );
            ha1 = md5.digest();

            String ha1Hex = StringFunctions.toHexString( ha1 );
            
            // Calculate H(A2)
            md5.reset();
            md5.update( "AUTHENTICATE".getBytes( "utf-8" ) );
            md5.update( COLON );
            
            //String host = "xmpp/" + this.xmppSettings.getStringData( XmppSettings.STRING_VALUE_HOSTNAME );
            md5.update( "xmpp/jabber.org".getBytes( "utf-8" ) );

            byte[] ha2 = md5.digest();
            String ha2Hex = StringFunctions.toHexString( ha2 );
            
            // Calculate response
            md5.reset();
            md5.update( ha1Hex.getBytes( "utf-8" ) );
            md5.update( COLON );
            md5.update( this.nonceValue.getBytes( "utf-8" ) );

            String newNc = StringFunctions.fillWithCharacter( String.valueOf( this.nc ), '0', 8 );
            md5.update( COLON );
            md5.update( newNc.getBytes( "utf-8" ) );
            md5.update( COLON );
            md5.update( this.cnonceValue.getBytes( "utf-8" ) );
            md5.update( COLON );
            md5.update( this.qop.getBytes( "utf-8" ) );

            md5.update( COLON );
            md5.update( ha2Hex.getBytes( "utf-8" ) );
            response = StringFunctions.toHexString( md5.digest() );
            
          } catch ( NoSuchAlgorithmException e )  {
        	  LogFunctions.log( "SaslHelper.computerResponseValue" , "NoSuchAlgorithmException", e.getMessage() );
            return null;
          } catch ( UnsupportedEncodingException e ) {
        	  LogFunctions.log( "SaslHelper.computerResponseValue" , "UnsupportedEncodingException", e.getMessage() );
        	  return null;
          }
  
          return response;
		
	}
	
	/**
	 * Reads the challenge an sets the values.
	 * 
	 * @param challenge The challenge to read.
	 */
	private void interpretChallenge( String challenge ) {
		String[] directives = challenge.split( "," );
		String[] directive = null;
		
		for( int i = 0; i < directives.length; i++ ) {
			directive = directives[ i ].split( "=" );
			if( directive[ 0 ].equals( "realm" ) ) {
				this.realmValue =  StringFunctions.removeQuotes( directive[ 1 ] );
			} else if( directive[ 0 ].equals( "nonce" ) ) {
				this.nonceValue =  StringFunctions.removeQuotes( directive[ 1 ] );
			} else if( directive[ 0 ].equals( "qop" ) ) {
				this.qop =  StringFunctions.removeQuotes( directive[ 1 ] );
			} else if( directive[ 0 ].equals( "stale" ) ) {
				 this.stale = Boolean.parseBoolean( directive[ 1 ] );
			} else if( directive[ 0 ].equals( "maxbuf" ) ) {
				this.maxbufValue = Integer.parseInt( directive[ 1 ] );
			} else if( directive[ 0 ].equals( "charset" ) ) {
				this.utf8 = true;
			} else if( directive[ 0 ].equals( "algorithm" ) ) {
				this.algorithm = true;
			} else if( directive[ 0 ].equals( "cipher" ) ) {
				this.cipherValue = StringFunctions.removeQuotes( directive[ 1 ] );
			}
		}
	}

	/**
	 * Returns the sasl stale value.
	 * 
	 * @return The sasl stale value.
	 */
	public boolean isStale() {
		return stale;
	}

	/**
	 * Returns the utf8 indicator.
	 * 
	 * @return <b>true</b> if utf8 is enabled, <b>false</b> otherwise.
	 */
	public boolean isUtf8() {
		return utf8;
	}

	/**
	 * Returns the sasl algorithm value.
	 * 
	 * @return The sasl algorithm value.
	 */
	public boolean isAlgorithm() {
		return algorithm;
	}

	/**
	 * Returns the sasl maxbuf value.
	 * 
	 * @return The sasl maxbuf value.
	 */
	public int getMaxbufValue() {
		return maxbufValue;
	}

	/**
	 * Returns the sasl cipher value.
	 * 
	 * @return The sasl cipher value.
	 */
	public String getCipherValue() {
		return cipherValue;
	}
	
}
